/******************************************************************
*
*	CyberUPnP for Java
*
*	Copyright (C) Satoshi Konno 2002-2004
*
*	File: ControlPoint.java
*
*	Revision:
*
*	11/18/02
*		- first revision.
*	05/13/03
*		- Changed to create socket threads each local interfaces.
*		  (HTTP, SSDPNotiry, SSDPSerachResponse)
*	05/28/03
*		- Changed to send m-serach packets from SSDPSearchResponseSocket.
*		  The socket doesn't bind interface address.
*		- SSDPSearchResponsSocketList that binds a port and a interface can't
*		  send m-serch packets of IPv6 on J2SE v 1.4.1_02 and Redhat 9.
*	07/23/03
*		- Suzan Foster (suislief)
*		- Fixed a bug. HOST field was missing.
*	07/29/03
*		- Synchronized when a device is added by the ssdp message.
*	09/08/03
*		- Giordano Sassaroli <sassarol@cefriel.it>
*		- Problem : when an event notification message is received and the message
*		            contains updates on more than one variable, only the first variable update
*		            is notified.
*		- Error :  the other xml nodes of the message are ignored
*		- Fix : add two methods to the NotifyRequest for extracting the property array
*                and modify the httpRequestRecieved method in ControlPoint
*	12/12/03
*		- Added a static() to initialize UPnP class.
*	01/06/04
*		- Added the following methods to remove expired devices automatically
*		  removeExpiredDevices()
*		  setExpiredDeviceMonitoringInterval()/getExpiredDeviceMonitoringInterval()
*		  setDeviceDisposer()/getDeviceDisposer()
*	04/20/04
*		- Added the following methods.
*		  start(String target, int mx) and start(String target).
*	06/23/04
*		- Added setNMPRMode() and isNMPRMode().
*	07/08/04
*		- Added renewSubscriberService().
*		- Changed start() to create renew subscriber thread when the NMPR mode is true.
*
*******************************************************************/

package org.cybergarage.upnp;

import java.util.Vector;
import java.util.logging.Logger;

import org.cybergarage.net.*;
import org.cybergarage.util.*;
import org.cybergarage.xml.*;
import org.cybergarage.http.*;

import org.cybergarage.upnp.control.*;
import org.cybergarage.upnp.ssdp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.event.*;

public class ControlPoint implements HTTPRequestListener
{
  private static Logger logger = Logger.getLogger("org.cybergarage.upnp");

	private final static int DEFAULT_EVENTSUB_PORT = 8058;
	private final static int DEFAULT_SSDP_PORT = 8008;
	private final static int DEFAULT_EXPIRED_DEVICE_MONITORING_INTERVAL = 60;
	
	private final static String DEFAULT_EVENTSUB_URI = "/eventSub";
	
	////////////////////////////////////////////////
	//	Member
	////////////////////////////////////////////////
	
	private SSDPNotifySocketList ssdpNotifySocketList;
	private SSDPSearchResponseSocketList ssdpSearchResponseSocketList;

	private SSDPNotifySocketList getSSDPNotifySocketList()
	{
		return ssdpNotifySocketList;
	}
	
	private SSDPSearchResponseSocketList getSSDPSearchResponseSocketList()
	{
		return ssdpSearchResponseSocketList;
	}



	////////////////////////////////////////////////
	//	Initialize
	////////////////////////////////////////////////
	
	static 
	{
		UPnP.initialize();
	}
	
	////////////////////////////////////////////////
	//	Constructor
	////////////////////////////////////////////////

	public ControlPoint(int ssdpPort, int httpPort)
	{
		ssdpNotifySocketList = new SSDPNotifySocketList();
		ssdpSearchResponseSocketList = new SSDPSearchResponseSocketList();
		
		setSSDPPort(ssdpPort);
		setHTTPPort(httpPort);
		
		setDeviceDisposer(null);
		setExpiredDeviceMonitoringInterval(DEFAULT_EXPIRED_DEVICE_MONITORING_INTERVAL);

		setRenewSubscriber(null);
				
		setNMPRMode(false);
		setRenewSubscriber(null);
	}

	public ControlPoint()
	{
		this(DEFAULT_SSDP_PORT, DEFAULT_EVENTSUB_PORT);
	}

	public void finalize()
	{
		stop();
	}



	////////////////////////////////////////////////
	// Mutex
	////////////////////////////////////////////////
	
	private Mutex mutex = new Mutex();
	
	public void lock()
	{
		mutex.lock();
	}
	
	public void unlock()
	{
		mutex.unlock();
	}
	
	////////////////////////////////////////////////
	//	Port (SSDP)
	////////////////////////////////////////////////

	private int ssdpPort = 0;
	
	public int getSSDPPort() {
		return ssdpPort;
	}

	public void setSSDPPort(int port) {
		ssdpPort = port;
	}

	////////////////////////////////////////////////
	//	Port (EventSub)
	////////////////////////////////////////////////

	private int httpPort = 0;
	
	public int getHTTPPort() {
		return httpPort;
	}

	public void setHTTPPort(int port) {
		httpPort = port;
	}
	
	////////////////////////////////////////////////
	//	NMPR
	////////////////////////////////////////////////

	private boolean nmprMode;
	
	public void setNMPRMode(boolean flag)
	{
		nmprMode = flag;
	}

	public boolean isNMPRMode()
	{
		return nmprMode;
	}
	
	////////////////////////////////////////////////
	//	Device List
	////////////////////////////////////////////////

  /**
   * Check if SSDP packet device type matches one of those passed to the
   * ControlPoint.start() method at startup.
   *
   * @param packet    SSDP packet from notify message or search response
   *
   * @return   true if device is recognized, otherwise false
   */
  public boolean isRecognizedDeviceTarget( SSDPPacket packet )
  {
    //
    // Get the device type string from the packet. If this is a NOTIFY packet,
    // this is the NT field.  If this is a search response packet, the 
    // USN field is used.
    //
    logger.info("Packet: " + packet.toString() );

    String packetDeviceType = packet.getNT();
    if( packetDeviceType == null || packetDeviceType.length() == 0 )
      packetDeviceType = packet.getUSN();

    if( packetDeviceType == null || packetDeviceType.length() == 0 )
    {
      logger.warning("Unknown SSDP packet type");
      return false;
    }

    for( int n = 0 ; n < deviceTargets.length ; n++ )
    {
      logger.info("PacketDevType: " + packetDeviceType + " devTarget " +
                  deviceTargets[n] );
      if( packetDeviceType.indexOf( deviceTargets[n] ) >= 0 )
        return true;
    }

    logger.info("device not recognized " );

    return false;
  }

	private NodeList devNodeList = new NodeList();

	private void addDevice(Node rootNode)
	{
		devNodeList.add(rootNode);
	}

  /**
   *  Upon receipt of a notify packet, check to see if this is a notify
   *  packet for a new device. If so, add the device to the list of devices
   *  being managed by the control point.
   *
   */ 
	private synchronized void addDevice(SSDPPacket ssdpPacket)
	{
    if (ssdpPacket.isRootDevice() == false)
      return;
			
    String usn = ssdpPacket.getUSN();
    String udn = USN.getUDN(usn);

    // Check if device already registered in control point. If so, update
    // the alive time
    Device dev = getDevice(udn);
    if (dev != null) {
      dev.setSSDPPacket(ssdpPacket);
      return;
    }
    
		String location = ssdpPacket.getLocation();
		try {	
			Parser parser = UPnP.getXMLParser();
			Node rootNode = parser.parse(location);
			Device rootDev = getDevice(rootNode);
			if (rootDev == null)
				return;
			rootDev.setSSDPPacket(ssdpPacket);

      // Attach perspective to Device object so device & service methods 
      // that are shared by Devices and Control points can tell at runtime
      // that this is the control point perspective
      rootDev.setInstancePerspective( UPnP.CONTROL_POINT );

			addDevice(rootNode);

      // After node is added, invoke the AddDeviceListener to notify high-level 
      // control point application that a new device has been added. (The 
      // control point application must implement the DeviceChangeListener interface
      // to receive the notifications)
      performAddDeviceListener( rootDev );

		}
		catch (ParserException e) {
			logger.warning(ssdpPacket.toString());
			logger.warning( e.toString() );
      logger.warning("Parser error reading XML device description - not adding device");
		}
	}


  /**
   *  'Force' the addition of a new device given the location of the 
   *  device description XML. 
   *
   *  This version exists for devices that have broken/flaky notification 
   *  implementations (early versions of Windows Media Connect for example)
   *
   *  @todo - This is a hackish implementation, since a fake SSDPPacket
   *  must be created. The whole tieing of the data model to the message
   *  transports needs to be redone for the Cyberlink package - it's too
   *  fragile.
   *
   */ 
	public synchronized void addDevice( String location, 
                                      String cacheControl )
	{
    // Create a fake SSDPPacket
    SSDPPacket ssdpPacket = new SSDPPacket();
    ssdpPacket.setNT( "upnp:rootdevice" );
    //ssdpPacket.setUSN( usn );
    ssdpPacket.setLocation( location );
    ssdpPacket.setCacheControl( cacheControl );
    ssdpPacket.setTimeStamp( System.currentTimeMillis() );

    logger.info("Forcing add of new device - location = " + location );

    
		try {	
			Parser parser = UPnP.getXMLParser();
			Node rootNode = parser.parse(location);
			Device rootDev = getDevice(rootNode);
			if (rootDev == null)
				return;
			rootDev.setSSDPPacket(ssdpPacket);

      // Attach perspective to Device object so device & service methods 
      // that are shared by Devices and Control points can tell at runtime
      // that this is the control point perspective
      rootDev.setInstancePerspective( UPnP.CONTROL_POINT );

			addDevice(rootNode);

      // After node is added, invoke the AddDeviceListener to notify high-level 
      // control point application that a new device has been added. (The 
      // control point application must implement the DeviceChangeListener interface
      // to receive the notifications)
      performAddDeviceListener( rootDev );

		}
		catch (ParserException e) {
			logger.warning( e.toString() );
      logger.warning("Parser error reading XML device description - not adding device");
		}
	}


	private Device getDevice(Node rootNode)
	{
		if (rootNode == null)
				return null;
		Node devNode = rootNode.getNode(Device.ELEM_NAME);
		if (devNode == null)
				return null;
		return new Device(rootNode, devNode);
	}

	public DeviceList getDeviceList()
	{
		DeviceList devList = new DeviceList();
		int nRoots = devNodeList.size();
		for (int n=0; n<nRoots; n++) {
			Node rootNode = devNodeList.getNode(n);
			Device dev = getDevice(rootNode);
			if (dev == null)
				continue;
			devList.add(dev);
		} 
		return devList;
	}

	public Device getDevice(String name)
	{
		int nRoots = devNodeList.size();
		for (int n=0; n<nRoots; n++) {
			Node rootNode = devNodeList.getNode(n);
			Device dev = getDevice(rootNode);
			if (dev == null)
				continue;
			if (dev.isDevice(name) == true)
				return dev;
			Device cdev = dev.getDevice(name);
			if (cdev != null)
				return cdev;
		} 
		return null;
	}

	public boolean hasDevice(String name)
	{
		return (getDevice(name) != null) ? true : false;
	}

	private void removeDevice(Node rootNode)
	{
    //
    // Invoke device removal listener prior to actual removal so Device node 
    // remains valid for the duration of the listener (application may want
    // to access the node)
    //
    Device dev = getDevice(rootNode);
    if( dev != null && dev.isRootDevice() )
      performRemoveDeviceListener( dev );

		devNodeList.remove(rootNode);
	}

	private void removeDevice(Device dev)
	{
		if (dev == null)
			return;
		removeDevice(dev.getRootNode());
	}
	
	private void removeDevice(String name)
	{
		Device dev = getDevice(name);
		removeDevice(dev);
	}

	private void removeDevice(SSDPPacket packet)
	{
		if (packet.isByeBye() == false)
			return;
		String usn = packet.getUSN();
		String udn = USN.getUDN(usn);
		removeDevice(udn);
	}

  /**
   *  Remove all devices. This is called whenever the control point is 
   *  stopped, since all the device communications are terminated.  When
   *  the control point is restarted, it only takes a few seconds to 
   *  fully rediscover them, so this is an ok strategy
   */
	public void removeAllDevices()
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++)
    {
			Device dev = devList.getDevice(n);
      logger.info("Removing device: " + dev.getFriendlyName() );
      removeDevice(dev);
    }
	}


	
	////////////////////////////////////////////////
	//	Expired Device
	////////////////////////////////////////////////
	
	private Disposer deviceDisposer;
	private long expiredDeviceMonitoringInterval;  // In seconds
	
	public void removeExpiredDevices()
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			if (dev.isExpired() == true) {
				logger.warning("Removing expired device = " + dev.getFriendlyName());
				removeDevice(dev);
			}
		}		
	}
	
  /**
   * Set the interval for monitoring of expired devices, in seconds
   */
	public void setExpiredDeviceMonitoringInterval(long intervalSec)
	{
		expiredDeviceMonitoringInterval = intervalSec;
	}

	public long getExpiredDeviceMonitoringInterval()
	{
		return expiredDeviceMonitoringInterval;
	}
	
	public void setDeviceDisposer(Disposer disposer)
	{
		deviceDisposer = disposer;
	}
	
	public Disposer getDeviceDisposer()
	{
		return deviceDisposer;
	}

	
	////////////////////////////////////////////////
	//	Notify
	////////////////////////////////////////////////

	private ListenerList deviceNotifyListenerList = new ListenerList();
	 	
	public void addNotifyListener(NotifyListener listener)
	{
		deviceNotifyListenerList.add(listener);
	}		

	public void removeNotifyListener(NotifyListener listener)
	{
		deviceNotifyListenerList.remove(listener);
	}		

	public void performNotifyListener(SSDPPacket ssdpPacket)
	{
		int listenerSize = deviceNotifyListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			NotifyListener listener = (NotifyListener)deviceNotifyListenerList.get(n);
			listener.deviceNotifyReceived(ssdpPacket);
		}
	}

	/////////////////////////////////////////////////////////////////////
  //
  // Device status changes (device added or removed) 
  // 
  // Applications that support the DeviceChangeListener interface are 
  // notified immediately when a device is added to, or removed from,
  // the control point.
  //
  /////////////////////////////////////////////////////////////////////

  Vector deviceChangeListenerList = new Vector();
  
	public void addDeviceChangeListener(DeviceChangeListener listener)
	{
		deviceChangeListenerList.add(listener);
	}		

	public void removeDeviceChangeListener(DeviceChangeListener listener)
	{
		deviceChangeListenerList.remove(listener);
	}		

	public void performAddDeviceListener( Device dev )
	{
		int listenerSize = deviceChangeListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			DeviceChangeListener listener = (DeviceChangeListener)deviceChangeListenerList.get(n);
			listener.deviceAdded( dev );
		}
  }

	public void performRemoveDeviceListener( Device dev )
	{
		int listenerSize = deviceChangeListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			DeviceChangeListener listener = (DeviceChangeListener)deviceChangeListenerList.get(n);
			listener.deviceRemoved( dev );
		}
  }
  
	/*
   *	Non-Notify HTTP Requests
   *
   *  This allows apps to install an HTTP listener for HTTP requests
   *  that are not event notifications (piggyback on the event notification
   *  HTTP server)
   */
	private ListenerList httpRequestListenerList = new ListenerList();
	 	
	public void addHttpRequestListener(HTTPRequestListener listener)
	{
		httpRequestListenerList.add(listener);
	}		

	public void removeHttpRequestListener(HTTPRequestListener listener)
	{
		httpRequestListenerList.remove(listener);
	}		

	public void performRequestListener(HTTPRequest httpReq)
	{
		int listenerSize = httpRequestListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			HTTPRequestListener listener = 
        (HTTPRequestListener)httpRequestListenerList.get(n);
			listener.httpRequestReceived(httpReq);
		}
	}		


	////////////////////////////////////////////////
	//	SearchResponse
	////////////////////////////////////////////////

	private ListenerList deviceSearchResponseListenerList = new ListenerList();
	 	
	public void addSearchResponseListener(SearchResponseListener listener)
	{
		deviceSearchResponseListenerList.add(listener);
	}		

	public void removeSearchResponseListener(SearchResponseListener listener)
	{
		deviceSearchResponseListenerList.remove(listener);
	}		

	public void performSearchResponseListener(SSDPPacket ssdpPacket)
	{
		int listenerSize = deviceSearchResponseListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			SearchResponseListener listener = (SearchResponseListener)deviceSearchResponseListenerList.get(n);
			listener.deviceSearchResponseReceived(ssdpPacket);
		}
	}

  
	/**
   *  Handle incoming NOTIFY SSDP message.  If message is an alive message,
   *  and the control point is controlling devices of the specified type
   *  (the notification type, or 'NT' field), add/remove the device to the 
   *  control point if it hasn't already been added/removed.  Also pass 
   *  the notify packet on to an application-level listener if one was 
   *  installed
   */
	public void notifyReceived(SSDPPacket packet)
	{
    //if ( isRecognizedDeviceTarget( packet ) == true )
    if ( packet.isRootDevice() == true )
    {
			if (packet.isAlive() == true)
				addDevice(packet);
			if (packet.isByeBye() == true) 
				removeDevice(packet);
		}

    // Peform application-level listener 
		performNotifyListener(packet);
	}

	public void searchResponseReceived(SSDPPacket packet)
	{
    //if ( isRecognizedDeviceTarget( packet ) == true )
    if ( packet.isRootDevice() == true )
			addDevice(packet);

		performSearchResponseListener(packet);
	}

	////////////////////////////////////////////////
	//	M-SEARCH
	////////////////////////////////////////////////

	private int serchMx = SSDP.DEFAULT_MSEARCH_MX;

	public int getSerchMx()
	{
		return serchMx;
	}

	public void setSerchMx(int mx) 
	{
		serchMx = mx;
	}

	public void search(String target, int mx)
	{
		SSDPSearchRequest msReq = new SSDPSearchRequest(target, mx);
		SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
		ssdpSearchResponseSocketList.post(msReq);
	}

	public void search(String target)
	{
		search(target, SSDP.DEFAULT_MSEARCH_MX);
	}

	public void search()
	{
		search(ST.ROOT_DEVICE, SSDP.DEFAULT_MSEARCH_MX);
	}

	/*
   *	Event subscription HTTPServer.  A set of HTTP servers is used
   *  to service incoming event messages, one server per network
   *  interface.  The servers are shared by all services on a given
   *  network segment.  In other words, if you machine has a single
   *  network interface, only one HTTP server is used, and all events
   *  from all connected UPnP devices arrive on that server's port
   */

  // Instantiate the list of HTTP servers, one per host interface
	private HTTPServerList httpServerList = new HTTPServerList();
	
	public HTTPServerList getHTTPServerList()
	{
		return httpServerList;
	}
		
  /**
   * Process the incoming event messages (HTTP NOTIFY)
   *
   * Pass other HTTP messages (mostly GET's) on to application if app 
   * installed a listener (this mod was originally done to allow A/V control 
   * point application to serve up their own dynamically-generated
   * M3U playlists)
   *
   */  
	public void httpRequestReceived(HTTPRequest httpReq)
	{
    if (Debug.isOn() == true)
			httpReq.print();
		
		// Thanks for Giordano Sassaroli <sassarol@cefriel.it> (09/08/03)
		if (httpReq.isNotifyRequest() == true)
    {
      //logger.info("Incoming event req:\n" + httpReq.toString() );
			//httpReq.print();

      // Read the request body from the underlying socket
      NotifyRequest notifyReq = new NotifyRequest(httpReq);

			String uuid = notifyReq.getSID();
			long seq = notifyReq.getSEQ();

      //
      // Get the property list for the event message. This routine ends
      // up invoking the XML parser in the SOAPRequest base class.  Note 
      // that the parser removes one 'layer' of XML escaping for the 
      // property values
      //
			PropertyList props = notifyReq.getPropertyList();
      if( props == null )
      {
        logger.warning("Badly formed notify request - discarding");
        // Still return OK for now so as to not cause the offending device
        // any problems  (TODO: revisit this)
        httpReq.returnOK();
        return;
      }

      // Invoke event listener once for each separate property
			int propCnt = props.size();
			for (int n = 0; n < propCnt; n++) {
				Property prop = props.getProperty(n);
				String varName = prop.getName();
				String varValue = prop.getValue();
				performEventListener(uuid, seq, varName, varValue);
			}
			httpReq.returnOK();
			return;
 		}
    else 
    {
      // Pass non-notify requests to app-level if app installed listener
      performRequestListener(httpReq);
    }
		
		httpReq.returnBadRequest();
	}

	////////////////////////////////////////////////
	//	Event Listener 
	////////////////////////////////////////////////

	private ListenerList eventListenerList = new ListenerList();
	 	
	public void addEventListener(EventListener listener)
	{
		eventListenerList.add(listener);
	}		

	public void removeEventListener(EventListener listener)
	{
		eventListenerList.remove(listener);
	}		

	public void performEventListener(String uuid, long seq,
                                   String name, String value)
	{
		int listenerSize = eventListenerList.size();
		for (int n=0; n<listenerSize; n++) {
			EventListener listener = (EventListener)eventListenerList.get(n);
			listener.eventNotifyReceived(uuid, seq, name, value);
		}
	}
	
	////////////////////////////////////////////////
	//	Subscription 
	////////////////////////////////////////////////

  /**
   * Recommended subscription renewal interval for NMPR compliance 
   * is 120 second. Specify 60 sec more here for headroom. Resubscribe
   * thread subtracts the 60 sec, so this works out
   */
	public final static int DEFAULT_SUBSCRIPTION_PERIOD_SECS = 180;
  
  int subscriptionPeriodSec =  DEFAULT_SUBSCRIPTION_PERIOD_SECS;

  public int getSubscriptionPeriodSec()
  {
    return subscriptionPeriodSec;
  }
  public void setSubscriptionPeriodSec( int periodSec )
  {
    subscriptionPeriodSec = periodSec;
  }

  // Subscription event listener lets control points expose subscription
  // events to users. Useful for debugging-type windows.
	private ListenerList subChangeListenerList = new ListenerList();
	 	
	public void 
  addSubscriptionChangeListener(SubscriptionChangeListener listener)
	{
		subChangeListenerList.add(listener);
	}		

	public void
  removeSubscriptionChangeListener(SubscriptionChangeListener listener)
	{
		subChangeListenerList.remove(listener);
	}		

	public void 
  performSubscriptionRequestListener( Service service, 
                                      SubscriptionRequest request )
	{
		int listenerSize = subChangeListenerList.size();
		for (int n=0; n<listenerSize; n++)
    {
			SubscriptionChangeListener listener = 
        (SubscriptionChangeListener)subChangeListenerList.get(n);
			listener.subscriptionRequestSent( service, request );
		}
	}

	public void 
  performSubscriptionResponseListener( Service service,
                                       SubscriptionResponse response )
	{
		int listenerSize = subChangeListenerList.size();
		for (int n=0; n<listenerSize; n++)
    {
			SubscriptionChangeListener listener = 
        (SubscriptionChangeListener)subChangeListenerList.get(n);
			listener.subscriptionResponseReceived( service, response );
		}
	}

	private String eventSubURI = DEFAULT_EVENTSUB_URI;

	public String getEventSubURI()
	{
		return eventSubURI;
	}

	public void setEventSubURI(String url)
	{
		eventSubURI = url;
	}

	private String getEventSubCallbackURL(String host)
	{
		return HostInterface.getHostURL(host, getHTTPPort(), getEventSubURI());
	}
	
  /**
   * Subscribe to service.  Subscription request/response pair looks like:
   *
   *  Request:
   *
   *    SUBSCRIBE <eventURL> HTTP/1.1
   *    Host: <publisherHost>:<publisherPort>
   *    Callback: <deliveryURL>
   *    NT: upnp:event
   *    Timeout: Second-<requestedDuration>    (-1 for infinite)
   *
   *  Response:
   *
   *    HTTP/1.1 200 OK
   *    Date: <Date>
   *    Server: OS/version UPnP/1.0 product/version
   *    SID: uuid:<subscriptionId>
   *    Timeout: Second-<actual granted subscription duration>
   *   
   */ 
	public boolean subscribe(Service service, long timeout)
	{
    // If service is already subscribed, re-subscribe
		if (service.isSubscribed() == true) {
			String sid = service.getSID();
			return renewSubscription(service, sid, timeout);
		}
		
		Device rootDev = service.getRootDevice();
		if (rootDev == null)
			return false;
		String ifAddress = rootDev.getInterfaceAddress();		 

		SubscriptionRequest subReq = new SubscriptionRequest();
		subReq.setSubscribeRequest(service, getEventSubCallbackURL(ifAddress),
                               timeout);

    performSubscriptionRequestListener( service, subReq );

		SubscriptionResponse subRes = subReq.post();

    performSubscriptionResponseListener( service, subRes );
    
		if (subRes.getStatusCode() == HTTPStatus.OK)
    {
			service.setSID( subRes.getSID() );
			service.setTimeout( subRes.getTimeout() );
			return true;
		}

		service.clearSID();
		return false;
	}

	public boolean subscribe(Service service)
	{
		return subscribe(service, Subscription.INFINITE_VALUE);
	}

  /**
   *  Issue subscription renewal request for the given subscription id
   */
	public boolean renewSubscription(Service service, String sid, long timeout)
	{
		SubscriptionRequest subReq = new SubscriptionRequest();
		subReq.setRenewRequest(service, sid, timeout);
		if (Debug.isOn() == true)
			subReq.print();	

    performSubscriptionRequestListener( service, subReq );

		SubscriptionResponse subRes = subReq.post();

    performSubscriptionResponseListener( service, subRes );

		if (Debug.isOn() == true)
			subRes.print();	

		if (subRes.getStatusCode() == HTTPStatus.OK) {
			service.setSID( subRes.getSID() );  
			service.setTimeout( subRes.getTimeout() );
			return true;
		}

		service.clearSID();
		return false;
	}

  /**
   *  Issue subscription renewal request for the given subscription id,
   *  specifying an infinite duration
   */
	public boolean renewSubscription(Service service, String uuid)
	{
		return renewSubscription(service, uuid, Subscription.INFINITE_VALUE);
	}

	public boolean isSubscribed(Service service)
	{
		if (service == null)
			return false;
		return service.isSubscribed();
	}
	
	public boolean unsubscribe(Service service)
	{
		SubscriptionRequest subReq = new SubscriptionRequest();
		subReq.setUnsubscribeRequest(service);

    performSubscriptionRequestListener( service, subReq );

		SubscriptionResponse subRes = subReq.post();

    performSubscriptionResponseListener( service, subRes );

		if (subRes.getStatusCode() == HTTPStatus.OK) {
			service.clearSID();
			return true;
		}
		return false;
	}

	public void unsubscribe(Device device)
	{
		ServiceList serviceList = device.getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			if (service.hasSID() == true)
				unsubscribe(service);
		}

		DeviceList childDevList = device.getDeviceList();
		int childDevCnt = childDevList.size();
		for (int n=0; n<childDevCnt; n++) {
			Device cdev = childDevList.getDevice(n);
			unsubscribe(cdev);
		}		
	}
	
	public void unsubscribe()
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			unsubscribe(dev);
		}		
	}

	////////////////////////////////////////////////
	//	getSubscriberService	
	////////////////////////////////////////////////

	public Service getSubscriberService(String uuid)
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Service service = dev.getSubscriberService(uuid);
			if (service != null)
				return service;
		}		
		return null;
	}
	
	////////////////////////////////////////////////
	//	renewSubscriberService	
	////////////////////////////////////////////////

  /**
   * Renew subscriptions for all services with the 'isSubscribed' flag
   * set for the specified device. Note that by default, no services
   * are subscribed - the control point must call the subscribe method
   * once at startup for each service it want's to subscribe to. After
   * that, a subscription renewal thread invokes this routine at 
   * regular intervals.
   * 
   * @param  dev        UPnP device 
   * @param  timeout    timeout, in seconds. A value of -1 is the
   *                    UPnP convention for an Infinite duration request,
   *                    and is what is used by the renewal thread.
   *                    An NMPR-compliant device will normally treat
   *                    the duration request as a 'don't care' and
   *                    return a non-infinite subscription timeout 
   */ 
	public void renewSubscriberService(Device dev, long timeout)
	{
		ServiceList serviceList = dev.getServiceList();

    int subscribedServiceCount = 0;
    int renewalFailureCount = 0;

		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++)
    {
			Service service = serviceList.getService(n);
			if (service.isSubscribed() == false)
				continue;

      subscribedServiceCount++;
      
			String sid = service.getSID();

      logger.finer("Renewing subscription for device '" + 
                  dev.getFriendlyName() + "' SID = " + sid +
                  " timeout = " + timeout );

			boolean isRenewed = renewSubscription(service, sid, timeout);
			if (isRenewed == false)
      {
        logger.warning("Subscription renewal failed - resubscribing");
				if( subscribe(service, timeout) == false )
          renewalFailureCount++;
      }
      else
      {
        logger.finer("Subscription renewal succeeded");
      }
		}
		
    //
    // OJN Mod - if there were subscribed services, and ALL of them failed
    // to resubscribe, assume device is dead and remove it from control
    // point
    //
    if( (subscribedServiceCount > 0) && 
        (renewalFailureCount == subscribedServiceCount) )
    {
      logger.warning("Device: " + dev.getFriendlyName() + 
           ": Couldn't resubscribe to *any* services - removing device" );
      
      removeDevice( dev );
      return;
    }

		DeviceList cdevList = dev.getDeviceList();
		int cdevCnt = cdevList.size();
		for (int n=0; n<cdevCnt; n++) {
			Device cdev = cdevList.getDevice(n);
			renewSubscriberService(cdev, timeout);
		}
	}
	
	public void renewSubscriberService(long timeout)
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			renewSubscriberService(dev, timeout);
		}		
	}
	
	public void renewSubscriberService()
	{
    // Added default timeout request of 180 sec if in NMPR mode. Default
    // resubscription period when in NMRP mode is 120 sec, so there is
    // 60 seconds of 'headroom' here (OJN)
    // Changed to use variable 6/20/05
    if( isNMPRMode() )
      renewSubscriberService( getSubscriptionPeriodSec() );
    else
      renewSubscriberService(Subscription.INFINITE_VALUE);
	}
	
	////////////////////////////////////////////////
	//	Subscriber
	////////////////////////////////////////////////
	
	private RenewSubscriber renewSubscriber;

	public void setRenewSubscriber(RenewSubscriber sub)
	{
		renewSubscriber = sub;
	}
	
	public RenewSubscriber getRenewSubscriber()
	{
		return renewSubscriber;	
	}
	

  String[] deviceTargets = null;

	/**
   * Start control point running for the given set of device targets
   *
   * @param  targets    Comma-separated list of target devices. Most 
   *                    common case is all root devices:
   *
   *                      "upnp:rootdevice"
   *
   *                    To support MediaServers and MediaRenderers (and
   *                    filter out WLAN gateways, etc...) use:
   *
   *                      "device:MediaServer:1,device:MediaRenderer:1"
   *
   * @param  mx         Max time for response
   */
	public boolean start(String targets, int mx)
	{
    // make list out of target string
    deviceTargets = targets.split(",");

		stop();
		
		////////////////////////////////////////
		// HTTP Server
		////////////////////////////////////////
		
		int retryCnt = 0;
		int bindPort = getHTTPPort();
		HTTPServerList httpServerList = getHTTPServerList();

    
		while (httpServerList.open(bindPort) == false) {
			retryCnt++;
			if (UPnP.SERVER_RETRY_COUNT < retryCnt)
				return false;
			setHTTPPort(bindPort + 1);
			bindPort = getHTTPPort();
		}
		httpServerList.addRequestListener(this);
		httpServerList.start();
		
		////////////////////////////////////////
		// Notify Socket
		////////////////////////////////////////
		
		SSDPNotifySocketList ssdpNotifySocketList = getSSDPNotifySocketList();
		if (ssdpNotifySocketList.open() == false)
			return false;
		ssdpNotifySocketList.setControlPoint(this);			
		ssdpNotifySocketList.start();
		
		////////////////////////////////////////
		// SeachResponse Socket
		////////////////////////////////////////
		
		int ssdpPort = getSSDPPort();
		retryCnt = 0;
		SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
		while (ssdpSearchResponseSocketList.open(ssdpPort) == false) {
			retryCnt++;
			if (UPnP.SERVER_RETRY_COUNT < retryCnt)
				return false;
			setSSDPPort(ssdpPort + 1);
			ssdpPort = getSSDPPort();
		}
		ssdpSearchResponseSocketList.setControlPoint(this);
		ssdpSearchResponseSocketList.start();

		////////////////////////////////////////
		// search for all specified device targets
		////////////////////////////////////////
		
    for( int n = 0 ; n < deviceTargets.length; n++ )
      search( deviceTargets[n], mx);
		
		////////////////////////////////////////
		// Disposer
		////////////////////////////////////////

		Disposer disposer = new Disposer(this);
		setDeviceDisposer(disposer);
		disposer.start();
				
		////////////////////////////////////////
		// Subscriber
		////////////////////////////////////////
		
		if (isNMPRMode() == true) {
			RenewSubscriber renewSub = new RenewSubscriber(this);
			setRenewSubscriber(renewSub);
			renewSub.start();
		}
		
		return true;
	}
	
	public boolean start(String targets)
	{
		return start(targets, SSDP.DEFAULT_MSEARCH_MX);
	}

	public boolean start()
	{
		return start(ST.ROOT_DEVICE, SSDP.DEFAULT_MSEARCH_MX);
	}
	
	public boolean stop()
	{ 
		unsubscribe();
		
		SSDPNotifySocketList ssdpNotifySocketList = getSSDPNotifySocketList();
		ssdpNotifySocketList.stop();
		ssdpNotifySocketList.close();
		ssdpNotifySocketList.clear();
		
		SSDPSearchResponseSocketList ssdpSearchResponseSocketList = getSSDPSearchResponseSocketList();
		ssdpSearchResponseSocketList.stop();
		ssdpSearchResponseSocketList.close();
		ssdpSearchResponseSocketList.clear();

		HTTPServerList httpServerList = getHTTPServerList();
		httpServerList.stop();
		httpServerList.close();
		httpServerList.clear();
			
		//
		// Stop the device 'disposer' thread that disposes of devices that 
    // have timed out
		//
		Disposer disposer = getDeviceDisposer();
		if (disposer != null) {
			disposer.stop();
			setDeviceDisposer(null);
		}
		
		//
		// Stop the subscription renewal thread that runs periodically and
    // keeps device event subscriptions up-to-date.
		//
		RenewSubscriber renewSub = getRenewSubscriber();
		if (renewSub != null) {
			renewSub.stop();
			setRenewSubscriber(null);
		}
		
    //
    // After everything shut down, remove the device objects themselves.
    // No sense keeping them around - they are quickly re-created when 
    // the control point is restarted.
    //
    removeAllDevices();

		return true;
	}

	////////////////////////////////////////////////
	//	print	
	////////////////////////////////////////////////
	
	public void print()
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		Debug.message("Device Num = " + devCnt);
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Debug.message("[" + n + "] " + dev.getFriendlyName() + ", " + dev.getLeaseTime() + ", " + dev.getElapsedTime());
		}		
	}
}
